// SDL2_13 [OpenGL ES Cube 7].nova
// Interactive OpenGL ES colour cube.
// By Robert Platt.



// Using namespace declarations.
using namespace lib.emscripten;
using namespace lib.math;
using namespace lib.opengl;
using namespace lib.sdl2;



class OpenGL_Quad
{
   public float b;
   public float t;
   public float l;
   public float r;
}



// The application class.
class SDL2_13_OpenGL_ES_Cube_7
{
   // Static data members.
   private static SDL_Window window;
   private static SDL_Renderer r;
   private static SDL_GLContext c;

   private static int initialSizeX, initialSizeY;
   private static int sizeX, sizeY;
   private static int prevSizeX, prevSizeY;  // Used for emscripten full-window mode.

   private static int Pmatrix;
   private static int Vmatrix;
   private static int Mmatrix;

   private static uint index_buffer;

   private static float[] proj_matrix;
   private static float[] mov_matrix;
   private static float[] view_matrix;

   private static int mouseX, mouseY;
   private static double spinX, spinY;
   private static bool mouseButtonDown;
   private static bool spinPaused;

   private static bool done;
   private static DisplayMode displayMode;
   private static SDL_Area prevWindowSize;  // Used for the desktop app full-screen mode.
   private static bool emscriptenActive;


   // Display mode enumeration.
   public enum DisplayMode : byte { Canvas, FullWindow, FullScreen }


   // Application class's "main" function.
   public static void main( String[] args )
   {
      Stream.writeLine( "SDL2_13 [OpenGL ES Cube 7].nova" );


      // Output the contols.
      Stream.write( "\nControls:\n"
                  + "   drag pointer / swipe screen = spin cube\n"
                  + "   left click / tap screen     = stop spinning\n"
                  + "   space                       = pause / resume spinning\n"
                  + "   f                           = toggle full screen mode\n"
                  + "   w                           = toggle full window mode (emscripten only)\n"
                  + "   esc                         = exit full screen / window mode\n\n" );


      // Initialise the class's data members.
      mouseX = 0; mouseY = 0;
      spinX = 0; spinY = 0;
      mouseButtonDown = false;
      spinPaused = false;
      done = false;
      initialSizeX = 640; initialSizeY = 480;
      sizeX = initialSizeX; sizeY = initialSizeY;
      prevSizeX = 0; prevSizeY = 0;
      emscriptenActive = Emscripten.isActive( );
      displayMode = emscriptenActive ? DisplayMode.Canvas : DisplayMode.FullWindow;


      // Disable the resize control.
      Emscripten.disableResizeControl( );

      // Show the cursor in full screen mode.
      Emscripten.setHideMousePointerCheckbox( false );

      // Set the full screen mode callback.
      Emscripten.setFullScreenChangeCallback( "SDL2_13_OpenGL_ES_Cube_7", fullScreenModeChange );

      // Set the resize event callback.
      Emscripten.setResizeEventCallback( "SDL2_13_OpenGL_ES_Cube_7", resizeEventCallback );

      // Setup SDL.
      SDL2.SDL_Init( SDL2.SDL_INIT_VIDEO );

      // Setup SDL for OpenGL.
      SDL2.SDL_GL_SetAttribute( SDL2.SDL_GL_CONTEXT_MAJOR_VERSION, 2 );
      SDL2.SDL_GL_SetAttribute( SDL2.SDL_GL_CONTEXT_MINOR_VERSION, 0 );
      SDL2.SDL_GL_SetAttribute( SDL2.SDL_GL_DOUBLEBUFFER, 1 );
      SDL2.SDL_GL_SetAttribute( SDL2.SDL_GL_DEPTH_SIZE, 24 );

      // Create the SDL2 window.
      window = SDL2.SDL_CreateWindow( "SDL2_13_OpenGL_ES_Cube_7",
                                      SDL2.SDL_WINDOWPOS_CENTERED,
                                      SDL2.SDL_WINDOWPOS_CENTERED,
                                      sizeX,
                                      sizeY,
                                      SDL2.SDL_WINDOW_OPENGL | SDL2.SDL_WINDOW_RESIZABLE );

      // Check for a null reference.
      if ( window == null )
      {
         // Output an error message.
         Stream.writeLine( "Failed to create window: " + SDL2.SDL_GetError( ) );

         // Abort the application.
         return;
      }

      r = SDL2.SDL_CreateRenderer( window,
                                   -1,
                                   SDL2.SDL_RENDERER_ACCELERATED | SDL2.SDL_RENDERER_PRESENTVSYNC );

      c = SDL2.SDL_GL_CreateContext( window );

      // Set the background colour.
      OpenGL.glClearColor( 0.0f, 0.0f, 0.0f, 1.0f );


/*      float[] vertices = {
         -1,-1,-1,   1,-1,-1,   1, 1,-1,  -1, 1,-1,  // Back face.
         -1,-1, 1,   1,-1, 1,   1, 1, 1,  -1, 1, 1,  // Front face.
         -1,-1,-1,  -1, 1,-1,  -1, 1, 1,  -1,-1, 1,  // Left face.
          1,-1,-1,   1, 1,-1,   1, 1, 1,   1,-1, 1,  // Right face.
         -1,-1,-1,  -1,-1, 1,   1,-1, 1,   1,-1,-1,  // Bottom face.
         -1, 1,-1,  -1, 1, 1,   1, 1, 1,   1, 1,-1   // Top face.
      };

      float[] colors = {
         1,0,0,  0,0,0,  0,0,1,  1,0,1,  // Back face:   bottom right = red,   bottom left = black,  top left = blue,     top right = magenta.
         1,1,0,  0,1,0,  0,1,1,  1,1,1,  // Front face:  bottom left = yellow, bottom right = green, top right = cyan,    top left = white.
         1,0,0,  1,0,1,  1,1,1,  1,1,0,  // left face:   bottom left = red,    top left = magenta,   top right = white,   bottom right = yellow.
         0,0,0,  0,0,1,  0,1,1,  0,1,0,  // Right face:  bottom right = black, top right = blue,     top left = cyan,     bottom left = green.
         1,0,0,  1,1,0,  0,1,0,  0,0,0,  // Bottom face: bottom left = red,    top left = yellow,    top right = green,   bottom right = black.
         1,0,1,  1,1,1,  0,1,1,  0,0,1   // Top face:    top left = magenta,   bottom left = white,  bottom right = cyan, top right = blue.
      };

      sbyte[] indices = {
         0,1,2,   0,2,3,  // Rear face.
         4,5,6,   4,6,7,  // Front face.
         0,3,7,   0,7,4,  // Left face.
         1,2,6,   1,6,5,  // Right face.
         0,4,5,   0,5,1,  // Bottom face.
         3,7,6,   3,6,2   // Top face.
      };*/


      // The cube has eight vertices.
      float[] vertices = {
         -1,-1,-1,  // Lower back left.
          1,-1,-1,  // Lower back right.
          1, 1,-1,  // Upper back right.
         -1, 1,-1,  // Upper back left.
         -1,-1, 1,  // Lower front left.
          1,-1, 1,  // Lower front right.
          1, 1, 1,  // Upper front right.
         -1, 1, 1   // Upper front left.
      };

      // Each of the above vertices has a corresponding colour.
      // NOTE: Interpolation was already enabled in the previous cube example,
      //       but it wasn't visible because the cube's faces had uniform colours.
      float[] colors = {
         1,0,0,  // Red.
         0,0,0,  // Black.
         0,0,1,  // Blue.
         1,0,1,  // Magenta.
         1,1,0,  // Yellow.
         0,1,0,  // Green.
         0,1,1,  // Cyan.
         1,1,1   // White.
      };

      // We can specify each triangle using its indices.
      // NOTE: Each cube face (quad) is made up of two triangles.
      sbyte[] indices = {
         0,1,2,   0,2,3,  // Rear face (red, black, blue magenta).
         4,5,6,   4,6,7,  // Front face (yellow, green, cyan, white).
         0,3,7,   0,7,4,  // Left face (red, magenta, white, yellow).
         1,2,6,   1,6,5,  // Right face (black, blue, cyan, green).
         0,4,5,   0,5,1,  // Bottom face (red, yellow, green, black).
         3,7,6,   3,6,2   // Top face (magenta, white, cyan, blue).
      };


      uint[] vbos = new uint[ 3 ];
      OpenGL.glGenBuffers( 3, vbos );

      // Create and store data into vertex buffer
      uint vertex_buffer = vbos[ 0 ];
      OpenGL.glBindBuffer( OpenGL.GL_ARRAY_BUFFER, vertex_buffer );
      OpenGL.glBufferData( OpenGL.GL_ARRAY_BUFFER, vertices, OpenGL.GL_STATIC_DRAW );

      // Create and store data into color buffer
      uint color_buffer = vbos[ 1 ];
      OpenGL.glBindBuffer( OpenGL.GL_ARRAY_BUFFER, color_buffer );
      OpenGL.glBufferData( OpenGL.GL_ARRAY_BUFFER, colors, OpenGL.GL_STATIC_DRAW );

      // Create and store data into index buffer
      index_buffer = vbos[ 2 ];
      OpenGL.glBindBuffer( OpenGL.GL_ELEMENT_ARRAY_BUFFER, index_buffer );
      OpenGL.glBufferData( OpenGL.GL_ELEMENT_ARRAY_BUFFER, indices, OpenGL.GL_STATIC_DRAW );


      // The shaders.
      String vertCode = "attribute vec3 position;                                               \n" +
                        "uniform mat4 Pmatrix;                                                  \n" +
                        "uniform mat4 Vmatrix;                                                  \n" +
                        "uniform mat4 Mmatrix;                                                  \n" +
                        "attribute vec3 color;                                                  \n" +  // The colour of the vertex.
                        "varying vec3 vColor;                                                   \n" +
                        "                                                                       \n" +
                        "void main( void )                                                      \n" +  // pre-built function
                        "{                                                                      \n" +
                        "   gl_Position = Pmatrix * Vmatrix * Mmatrix * vec4( position, 1.0 );  \n" +
                        "   vColor = color;                                                     \n" +
                        "}                                                                      \n";

      String fragCode = "precision mediump float;                \n" +
                        "varying vec3 vColor;                    \n" +
                        "                                        \n" +
                        "void main( void )                       \n" +
                        "{                                       \n" +
                        "   gl_FragColor = vec4( vColor, 1.0 );  \n" +
                        "}                                       \n";


      uint vertShader = OpenGL.glCreateShader( OpenGL.GL_VERTEX_SHADER );
      OpenGL.glShaderSource( vertShader, vertCode );
      OpenGL.glCompileShader( vertShader );

      uint fragShader = OpenGL.glCreateShader( OpenGL.GL_FRAGMENT_SHADER );
      OpenGL.glShaderSource( fragShader, fragCode );
      OpenGL.glCompileShader( fragShader );

      uint shaderProgram = OpenGL.glCreateProgram( );
      OpenGL.glAttachShader( shaderProgram, vertShader );
      OpenGL.glAttachShader( shaderProgram, fragShader );
      OpenGL.glLinkProgram( shaderProgram );
      OpenGL.glUseProgram( shaderProgram );

      // Position.
      OpenGL.glBindBuffer( OpenGL.GL_ARRAY_BUFFER, vertex_buffer );
      int position = OpenGL.glGetAttribLocation( shaderProgram, "position" );
      OpenGL.glVertexAttribPointer( (uint)position, 3, OpenGL.GL_FLOAT, false, 0, 0 ) ;
      OpenGL.glEnableVertexAttribArray( (uint)position );

      // Colour.
      OpenGL.glBindBuffer( OpenGL.GL_ARRAY_BUFFER, color_buffer );
      int color = OpenGL.glGetAttribLocation( shaderProgram, "color" );
      OpenGL.glVertexAttribPointer( (uint)color, 3, OpenGL.GL_FLOAT, false, 0, 0 ) ;
      OpenGL.glEnableVertexAttribArray( (uint)color );

      // Get the uniform locations.
      Pmatrix = OpenGL.glGetUniformLocation( shaderProgram, "Pmatrix" );
      Vmatrix = OpenGL.glGetUniformLocation( shaderProgram, "Vmatrix" );
      Mmatrix = OpenGL.glGetUniformLocation( shaderProgram, "Mmatrix" );


      calculateProjectionMatrix( );

      mov_matrix = { 1,0,0,0, 0,1,0,0, 0,0,1,0, 0,0,0,1 };
      view_matrix = { 1,0,0,0, 0,1,0,0, 0,0,1,0, 0,0,0,1 };

      // Translating z.
      view_matrix[ 14 ] -= 5;  // zoom.

      // Orientate the cube.
      rotateY( mov_matrix, 45.0f );
      rotateX( mov_matrix, 30.0f );


      // Check for the emscripten environment.
      if ( emscriptenActive )
      {
         int simulate_infinite_loop = 1;  // Call the function repeatedly.
         int fps = -1;  // Call the function as fast as the browser wants to render (typically 60fps).
         Emscripten.setMainLoop( renderFrame, fps, simulate_infinite_loop );
      }
      else
      {
         // Rendering and event processing loop.
         do
         {
            renderFrame( );
         }
         while( !done );
      }


      // Shutdown app.
      OpenGL.glDeleteProgram( shaderProgram );
      OpenGL.glDeleteShader( vertShader );
      OpenGL.glDeleteShader( fragShader );
      OpenGL.glDeleteBuffers( 3, vbos );
      SDL2.SDL_GL_DeleteContext( c );
      SDL2.SDL_DestroyRenderer( r );
      SDL2.SDL_DestroyWindow( window );
      SDL2.SDL_Quit( );
   }



   // Emscripten full screen mode callback.
   public static bool fullScreenModeChange( int eventType, EmscriptenFullScreenChangeEvent e, Object userObject )
   {
/*      Stream.writeLine( "fullScreenModeChange - called" );
      Stream.writeLine( "eventType = " + Integer.toString( eventType ) );
      Stream.writeLine( "isFullscreen = " + Boolean.toString( e.isFullScreen ) );
      Stream.writeLine( "fullscreenEnabled = " + Boolean.toString( e.fullScreenEnabled ) );
      Stream.writeLine( "nodeName = " + e.nodeName );
      Stream.writeLine( "id = " + e.id );
      Stream.writeLine( "userObject = " + (String)userObject );*/


      // Abort if in emscripten full-window mode.
      if ( displayMode == DisplayMode.FullWindow )
      {
         return true;
      }


      // Check for full screen mode.
      if ( e.isFullScreen )
      {
         // Update the size of the window.
         sizeX = e.elementWidth;
         sizeY = e.elementHeight;
      }
      else
      {
         // Update the size of the window.
         sizeX = initialSizeX;
         sizeY = initialSizeY;
      }


      // Set the canvas size.
      Emscripten.setCanvasSize( sizeX, sizeY );

      // Set the window size.
      SDL2.SDL_SetWindowSize( window, sizeX, sizeY );

      // Recalculate the projection matrix.
      calculateProjectionMatrix( );


      // Update the display mode state.
      if ( e.isFullScreen )
         displayMode = DisplayMode.FullScreen;
      else
         displayMode = emscriptenActive ? DisplayMode.Canvas : DisplayMode.FullWindow;


      // Return 'true' for handling the event.
      return true;
   }



   // Emscripten resize event callback, used for full window mode.
   public static bool resizeEventCallback( int eventType, EmscriptenUiEvent uiEvent, Object userObject )
   {
      // Diagnostic code.
/*      Stream.writeLine( "resizeEventCallback - called" );
      Stream.writeLine( "eventType = " + Integer.toString( eventType ) );
      Stream.writeLine( "uiEvent.detail = " + Integer.toString( uiEvent.detail ) );
      Stream.writeLine( "uiEvent.documentBodyClientWidth = " + Integer.toString( uiEvent.documentBodyClientWidth ) );
      Stream.writeLine( "uiEvent.documentBodyClientHeight = " + Integer.toString( uiEvent.documentBodyClientHeight ) );
      Stream.writeLine( "uiEvent.windowInnerWidth = " + Integer.toString( uiEvent.windowInnerWidth ) );
      Stream.writeLine( "uiEvent.windowInnerHeight = " + Integer.toString( uiEvent.windowInnerHeight ) );
      Stream.writeLine( "uiEvent.windowOuterWidth = " + Integer.toString( uiEvent.windowOuterWidth ) );
      Stream.writeLine( "uiEvent.windowOuterHeight = " + Integer.toString( uiEvent.windowOuterHeight ) );
      Stream.writeLine( "uiEvent.scrollTop = " + Integer.toString( uiEvent.scrollTop ) );
      Stream.writeLine( "uiEvent.scrollLeft = " + Integer.toString( uiEvent.scrollLeft ) );
      Stream.writeLine( "userObject = " + (String)userObject );*/


      // Check for full-window mode.
      if ( displayMode == DisplayMode.FullWindow )
      {
         // Get the new size.
         sizeX = uiEvent.windowInnerWidth;
         sizeY = uiEvent.windowInnerHeight;

         // Update the canvas size.
         Emscripten.setCanvasSize( sizeX, sizeY );

         // Set the window size.
         SDL2.SDL_SetWindowSize( window, sizeX, sizeY );

         // Recalculate the projection matrix.
         calculateProjectionMatrix( );
      }


      // Return 'true' for handling the event.
      return true;
   }



   public static void calculateProjectionMatrix( )
   {
      // Calculate the projection matrix.
      // float *proj_matrix = get_projection( 30, sizeX / sizeY, 1, 100 );
//      float angle = 30.0f, aspectRatio = sizeX / sizeX, zMin = 1.0f, zMax = 100.0f;
//      float ang = Math.tan( ( angle * 0.5f ) * (float)Math.PI / 180.0f );  // angle * 0.5.

/*      proj_matrix = {
            0.5f / ang, 0 , 0, 0,
            0, 0.5f * aspectRatio / ang, 0, 0,
            0, 0, -( zMax + zMin ) / ( zMax - zMin ), -1,
            0, 0, ( -2.0f * zMax * zMin ) / ( zMax - zMin ), 0 
      };*/


      float fovyInDegrees = 45.0f;
      float aspectRatio = (float)sizeX / (float)sizeY;
      float near = 1.0f;
      float far = 100.0f;
      proj_matrix = { 1,0,0,0, 0,1,0,0, 0,0,1,0, 0,0,0,1 };

//      setProjectionMatrix( 30.0f, 1.0f, 100.0f, proj_matrix );
      OpenGL_Quad q = gluPerspective( fovyInDegrees, aspectRatio, near, far );
      glFrustum( q, near, far, proj_matrix );
   }


   public static void setProjectionMatrix( float angleOfView, float near, float far, float[] m )
   {
      // Set the basic projection matrix.
      float scale = 1.0f / Math.tan( angleOfView * 0.5f * (float)Math.PI / 180.0f );
      m[  0 ] = scale;  // Scale the x coordinates of the projected point.
      m[  5 ] = scale;  // Scale the y coordinates of the projected point.
      m[ 10 ] = -far / (far - near);  // Used to remap z to [ 0, 1 ].
      m[ 11 ] = -1.0f;  // Set w = -z.
      m[ 14 ] = -far * near / (far - near);  // Used to remap z [ 0, 1 ].
      m[ 15 ] = 0.0f;
   }


   // Compute screen coordinates.
   public static OpenGL_Quad gluPerspective( float fovY, float aspect, float zNear, float zFar )
   {
      float scale = Math.tan( fovY * 0.5f * (float)Math.PI / 180.0f ) * zNear;

      OpenGL_Quad q = new OpenGL_Quad( );

      q.r = aspect * scale;
      q.l = -q.r;
      q.t = scale;
      q.b = -q.t;

      return q;
   }


   // Set the OpenGL perspective projection matrix.
   public static void glFrustum( OpenGL_Quad q, float n, float f, float[] m )
   {
      // Set OpenGL perspective projection matrix.
      m[  0 ] = 2 * n / ( q.r - q.l );
      m[  1 ] = 0;
      m[  2 ] = 0;
      m[  3 ] = 0;

      m[  4 ] = 0;
      m[  5 ] = 2 * n / ( q.t - q.b );
      m[  6 ] = 0;
      m[  7 ] = 0;

      m[  8 ] = ( q.r + q.l ) / ( q.r - q.l );
      m[  9 ] = ( q.t + q.b ) / ( q.t - q.b );
      m[ 10 ] = -( f + n ) / ( f - n );
      m[ 11 ] = -1;

      m[ 12 ] = 0;
      m[ 13 ] = 0;
      m[ 14 ] = -2 * f * n / ( f - n );
      m[ 15 ] = 0;
   }


   public static void onWindowEvent( SDL_WindowEvent e )
   {
//      Stream.writeLine( "SDL_WINDOWEVENT" );

      // Check for a non null reference after the object cast.
      if ( e != null )
      {
         switch ( e.event )
         {
            case SDL2.SDL_WINDOWEVENT_RESIZED :
            {
//               Stream.writeLine( "SDL_WINDOWEVENT_RESIZED" );

               // Update the size of the window.
               sizeX = e.data1;
               sizeY = e.data2;

               // Recalculate the projection matrix.
               calculateProjectionMatrix( );

               break;
            }
         }
      }
   }


   public static void onMouseButtonDown( SDL_MouseButtonEvent e )
   {
//      Stream.writeLine( "SDL_MOUSEBUTTONDOWN" );

      // Check for a non null reference after the object cast.
      if ( e != null )
      {
//         byte b = e.button;
//         Stream.writeLine( Byte.toString( b ) );

         if ( e.button == SDL2.SDL_BUTTON_LEFT )
         {
//            Stream.writeLine( "Left button pressed." );

            // Update the state of the left mouse button.
            mouseButtonDown = true;

            // Stop the cube from spinning.
            spinX = spinY = 0;
         }
      }
   }


   public static void onMouseButtonUp( SDL_MouseButtonEvent e )
   {
//      Stream.writeLine( "SDL_MOUSEBUTTONUP" );

      // Check for a non null reference after the object cast.
      if ( e != null )
      {
//         Byte b = new Byte( e.button );
//         Stream.writeLine( b.toString( ) );

         if ( e.button == SDL2.SDL_BUTTON_LEFT )
         {
//            Stream.writeLine( "Left button released." );

            // Update the state of the left mouse button.
            mouseButtonDown = false;
         }
      }
   }


   public static void onMouseMotion( SDL_MouseMotionEvent e )
   {
//      Stream.writeLine( "SDL_MOUSEMOTION" );

      // Check for a non null reference after the object cast.
      if ( e != null )
      {
         int x = e.x;
         int y = e.y;
//         Stream.writeLine( "x = " + Integer.toString( x ) + " y = " + Integer.toString( y ) );

         if ( ( e.state & SDL2.SDL_BUTTON_LMASK ) != 0 )
         {
//            Stream.writeLine( "Left button pressed (mouse motion)." );
//            SDL_Log( "Mouse Button 1 (left) is pressed." );

            spinX = (double)( y - mouseY ) / 2.0;
            spinY = (double)( x - mouseX ) / 2.0;

            // Orientate the cube.
            rotateCube( mov_matrix );

            mouseButtonDown = true;
            spinPaused = false;  // Resume spinning.
         }

         mouseX = x;
         mouseY = y;
      }
   }


   public static void renderFrame( )
   {
      /////////////////////////
      // SDL event handling. //
      /////////////////////////
      // Try to get an SDL event.
      SDL_Event e = SDL2.SDL_PollEvent( );

      // Check for a non null reference.
      if ( e != null )
      {
         // Switch on the type of the event.
         switch ( e.id )
         {
            case SDL2.SDL_WINDOWEVENT :
            {
               onWindowEvent( (SDL_WindowEvent)e );

               break;
            }

            case SDL2.SDL_MOUSEBUTTONDOWN :
            {
               onMouseButtonDown( (SDL_MouseButtonEvent)e );

               break;
            }

            case SDL2.SDL_MOUSEBUTTONUP :
            {
               onMouseButtonUp( (SDL_MouseButtonEvent)e );

               break;
            }

            case SDL2.SDL_MOUSEMOTION :
            {
               onMouseMotion( (SDL_MouseMotionEvent)e );

               break;
            }

            case SDL2.SDL_KEYDOWN :
            {
               // Cast to a keyboard event.
               SDL_KeyboardEvent kbEvent = (SDL_KeyboardEvent)e;

               // Switch on the event key.
               switch ( kbEvent.sym )
               {
                  case SDL2.SDLK_f :
                  {
//                     Stream.writeLine( "'f' key pressed." );

                     // Abort if in emscripten full window mode.
                     if ( emscriptenActive && ( displayMode == DisplayMode.FullWindow ) )
                     {
                        break;
                     }

                     // Toggle full screen mode.
                     toggleFullScreenMode( );

                     break;
                  }

                  case SDL2.SDLK_w :
                  {
//                     Stream.writeLine( "'w' key pressed." );

                     // Only allow if using emscripten and not in full screen mode.
                     if ( emscriptenActive && ( displayMode != DisplayMode.FullScreen ) )
                     {
                        // Toggle full window mode.
                        toggleFullWindowMode( );
                     }

                     break;
                  }

                  case SDL2.SDLK_SPACE :
                  {
//                     Stream.writeLine( "'SPACE' key pressed." );

                     // Pause / resume spinning.
                     spinPaused = !spinPaused;

                     break;
                  }

                  case SDL2.SDLK_ESCAPE :
                  {
//                     Stream.writeLine( "'ESC' key pressed." );

                     // Exit full screen mode for the desktop app.
                     if ( !emscriptenActive && ( displayMode == DisplayMode.FullScreen ) )
                        toggleFullScreenMode( );

                     // Exit full window mode for the emscripten app.
                     if ( emscriptenActive && ( displayMode == DisplayMode.FullWindow ) )
                        toggleFullWindowMode( );

                     break;
                  }
               }

               break;
            }

            case SDL2.SDL_QUIT :
            {
//               Stream.writeLine( "SDL_QUIT" );

               done = true;

               break;
            }
         }
      }


      ////////////////////
      // Spin the cube. //
      ////////////////////
      // Check if the mouse button is up and spinning not paused.
      if ( ( mouseButtonDown == false ) && ( spinPaused == false ) )
      {
         // Spin the cube.
         rotateCube( mov_matrix );
      }


      //////////////////////////////////////////////////
      // Validate the canvas size in emscripten mode. //
      //////////////////////////////////////////////////
      // Check for emscripten mode.
      if ( emscriptenActive )
      {
         // Get the dimensions of the canvas.
         int canvasWidth = Emscripten.getCanvasWidth( );
         int canvasHeight = Emscripten.getCanvasHeight( );

         // Abort drawing this frame if we have a zero width or zero height canvas.
         // NOTE: This can occur when switching to full-screen mode.
         if ( ( canvasWidth == 0 ) || ( canvasHeight == 0 ) )
         {
            return;
         }

         // Check the canvas size before drawing (diagnostic code).
         if ( ( canvasWidth != sizeX ) || ( canvasHeight != sizeY ) )
         {
            Stream.writeLine( "CANVAS SIZE MISMATCH DETECTED" );
            Stream.writeLine( "canvasWidth" + Integer.toString( canvasWidth ) );
            Stream.writeLine( "canvasHeight" + Integer.toString( canvasHeight ) );
            Stream.writeLine( "sizeX" + Integer.toString( sizeX ) );
            Stream.writeLine( "sizeY"  + Integer.toString( sizeY ) );
         }
      }


      ///////////////////////////////
      // Render the current frame. //
      ///////////////////////////////
      OpenGL.glEnable( OpenGL.GL_DEPTH_TEST );
      OpenGL.glDepthFunc( OpenGL.GL_LESS );
      OpenGL.glClearColor( 0.5f, 0.5f, 0.5f, 1.0f );  // Grey colour.
      OpenGL.glClearDepth( 1.0 );

      OpenGL.glViewport( 0, 0, sizeX, sizeY );
      OpenGL.glClear( OpenGL.GL_COLOR_BUFFER_BIT | OpenGL.GL_DEPTH_BUFFER_BIT );

      OpenGL.glUniformMatrix4fv( Pmatrix, 1, false, proj_matrix );
      OpenGL.glUniformMatrix4fv( Vmatrix, 1, false, view_matrix );
      OpenGL.glUniformMatrix4fv( Mmatrix, 1, false, mov_matrix );

      OpenGL.glBindBuffer( OpenGL.GL_ELEMENT_ARRAY_BUFFER, index_buffer );
      OpenGL.glDrawElements( OpenGL.GL_TRIANGLES, 36, OpenGL.GL_UNSIGNED_BYTE );

      SDL2.SDL_GL_SwapWindow( window );
   }


   // Toggle full screen mode.
   public static void toggleFullScreenMode( )
   {
      // Check for emscripten mode.
      if ( emscriptenActive )
      {
         // Check the full screen mode flag.
         if ( displayMode == DisplayMode.FullScreen )
         {
            // Exit full screen mode.
            Emscripten.exitFullScreenMode( );
         }
         else
         {
            // Enter full screen mode.
            Emscripten.enterFullScreenMode( );
         }
      }
      else  // Nova SDL2 desktop app.
      {
         // Get the current mode.
         // NOTE: Using a local variable.
         bool fullScreenMode = ( SDL2.SDL_GetWindowFlags( window ) & SDL2.SDL_WINDOW_FULLSCREEN ) != 0;

         // Are we in windowed mode?
         if ( !fullScreenMode )
         {
            // Record the size of the window before switching to full screen mode.
            prevWindowSize = SDL2.SDL_GetWindowSize( window );
         }

         // Toggle the full screen mode flag.
         fullScreenMode = !fullScreenMode;

         // Update the SDL2 full screen mode.
         SDL2.SDL_SetWindowFullscreen( window, fullScreenMode? SDL2.SDL_WINDOW_FULLSCREEN : 0 );

         // Show the cursor.
//         SDL2.SDL_ShowCursor( (int)SDL2.SDL_ENABLE );

         // Check for full screen mode.
         if ( fullScreenMode )
         {
            // Get the display index.
            int displayIndex = SDL2.SDL_GetWindowDisplayIndex( window );

            // Get the size of the whole desktop (the display resolution).
            SDL_DisplayMode dm = SDL2.SDL_GetDesktopDisplayMode( displayIndex );

            // Check for a null reference.
            if ( dm == null )
            {
               // Output an error message.
               Stream.write( "Error getting desktop display mode\n" );

               // Abort the operation.
               return;
            }

            // Set the size to the current display.
            sizeX = dm.w;
            sizeY = dm.h;
         }
         else
         {
            // Revert to the previous window size.
            sizeX = prevWindowSize.w;
            sizeY = prevWindowSize.h;
         }

         // Set the window size.
         SDL2.SDL_SetWindowSize( window, sizeX, sizeY );

         // Recalculate the projection matrix.
         calculateProjectionMatrix( );

         // Update the display mode state.
         if ( fullScreenMode )
            displayMode = DisplayMode.FullScreen;
         else
            displayMode = emscriptenActive ? DisplayMode.Canvas : DisplayMode.FullWindow;
      }
   }


   // Toggle full window mode.
   public static void toggleFullWindowMode( )
   {
      // Check for full window mode.
      if ( displayMode == DisplayMode.FullWindow )
      {
         // Restore the previous canvas size.
         sizeX = prevSizeX;
         sizeY = prevSizeY;

         // Set the window size.
         SDL2.SDL_SetWindowSize( window, sizeX, sizeY );

         // Recalculate the projection matrix.
         calculateProjectionMatrix( );

         // Exit full window mode.
         Emscripten.exitFullWindowMode( );

         // Update the state.
         displayMode = DisplayMode.Canvas;
      }
      else
      {
         // Backup the previous canvas size.
         prevSizeX = sizeX;
         prevSizeY = sizeY;

         sizeX = Emscripten.getClientWidth( );
         sizeY = Emscripten.getClientHeight( );

         // Enter full window mode.
         Emscripten.enterFullWindowMode( );

         // Set the window size.
         SDL2.SDL_SetWindowSize( window, sizeX, sizeY );

         // Recalculate the projection matrix.
         calculateProjectionMatrix( );

         // Update the state.
         displayMode = DisplayMode.FullWindow;
      }
   }


   // Rotate the cube.
   public static void rotateCube( float[] mov_matrix )
   {
      // Rotate the rotation matrix.
      rotateY( mov_matrix, (float)spinY );
      rotateX( mov_matrix, (float)spinX );
   }


   public static void rotateZ( float[] m, float angle )
   {
      angle *= (float)( Math.PI / 180.0 );  // Convert to radians.

      float c = Math.cos( angle );
      float s = Math.sin( angle );
      float mv0 = m[ 0 ], mv4 = m[ 4 ], mv8 = m[ 8 ];

      m[ 0 ] = c * m[ 0 ] - s * m[ 1 ];
      m[ 4 ] = c * m[ 4 ] - s * m[ 5 ];
      m[ 8 ] = c * m[ 8 ] - s * m[ 9 ];

      m[ 1 ] = c * m[ 1 ] + s * mv0;
      m[ 5 ] = c * m[ 5 ] + s * mv4;
      m[ 9 ] = c * m[ 9 ] + s * mv8;
   }


   public static void rotateX( float[] m, float angle )
   {
      angle *= (float)( Math.PI / 180.0 );  // Convert to radians.

      float c = Math.cos( angle );
      float s = Math.sin( angle );
      float mv1 = m[ 1 ], mv5 = m[ 5 ], mv9 = m[ 9 ];

      m[ 1 ] = m[ 1 ] * c - m[ 2 ]  * s;
      m[ 5 ] = m[ 5 ] * c - m[ 6 ]  * s;
      m[ 9 ] = m[ 9 ] * c - m[ 10 ] * s;

      m[ 2 ]  = m[ 2 ]  * c + mv1 * s;
      m[ 6 ]  = m[ 6 ]  * c + mv5 * s;
      m[ 10 ] = m[ 10 ] * c + mv9 * s;
   }


   public static void rotateY( float[] m, float angle )
   {
      angle *= (float)( Math.PI / 180.0 );  // Convert to radians.

      float c = Math.cos( angle );
      float s = Math.sin( angle );
      float mv0 = m[ 0 ], mv4 = m[ 4 ], mv8 = m[ 8 ];

      m[ 0 ] = c * m[ 0 ] + s * m[ 2 ];
      m[ 4 ] = c * m[ 4 ] + s * m[ 6 ];
      m[ 8 ] = c * m[ 8 ] + s * m[ 10 ];

      m[ 2 ]  = c * m[ 2 ]  - s * mv0;
      m[ 6 ]  = c * m[ 6 ]  - s * mv4;
      m[ 10 ] = c * m[ 10 ] - s * mv8;
   }
}